﻿namespace Pyramid.Hardware.MicroArchitecture
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    public class AddressableRom
    {
        private readonly Trivalent[][] dataStore;
        private readonly IndexedOutputLine[] output;
        private readonly int busSize;
        private readonly IEnumerable<ILine> addressBus;

        public AddressableRom(IEnumerable<ILine> addressBus, int busSize, int capacity)
        {
            this.busSize = busSize;
            this.addressBus = addressBus;
            this.dataStore = new Trivalent[capacity][];

            this.output = new IndexedOutputLine[this.busSize];

            for (int lineIndex = 0; lineIndex < output.Length; lineIndex++)
            {
                output[lineIndex] = new IndexedOutputLine(this, lineIndex);
            }

            for (int dataIndex = 0; dataIndex < capacity; dataIndex++)
            {
                SetData(dataIndex, Enumerable.Repeat(Trivalent.Unknown, busSize));
            }

            foreach (var line in addressBus)
            {
                line.SignalChange += new EventHandler<SignalChangeEventArgs>(AddressBus_SignalChange);
            }
        }

        void AddressBus_SignalChange(object sender, SignalChangeEventArgs e)
        {
            UpdateAll();
        }

        private void UpdateAll()
        {
            foreach (var outputLine in this.output)
            {
                outputLine.Update();
            }
        }

        public void SetData(int index, ITernary data)
        {
            SetData(index, data.ToTritArray().Take(busSize));
        }

        public void SetData(int index, IEnumerable<Trivalent> data)
        {
            if (data.Count() != busSize)
            {
                throw new ArgumentException("data", "Trivalent collection is not the same size as data store bus size.");
            }

            this.dataStore[index] = data.ToArray();

            if (CurrentAddress == index)
            {
                UpdateAll();
            }
        }

        private Tryte CurrentAddress
        {
            get
            {
                Tryte result = 0;

                short tryteValue = 1;

                foreach (var line in addressBus)
                {
                    result += (Tryte)((short)line.Signal * tryteValue);

                    tryteValue *= 3;
                }

                return result;
            }
        }

        private class IndexedOutputLine
            : LineBase
        {
            private readonly int index;

            private readonly AddressableRom parentStore;

            public IndexedOutputLine(AddressableRom parentAddressable, int index)
            {
                this.parentStore = parentAddressable;
                this.index = index;
            }

            public void Update()
            {
                if (this.parentStore.CurrentAddress >= 0 && this.parentStore.CurrentAddress < this.parentStore.dataStore.Length)
                {
                    SetSignal(parentStore.dataStore[this.parentStore.CurrentAddress][this.index]);
                }
                else
                {
                    SetSignal(Trivalent.Unknown);
                }
            }
        }

        public IEnumerable<ILine> OutputBus
        {
            get
            {
                return this.output;
            }
        }
    }
}
